#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <ctype.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>

#define NUM 10001
#define OPTION 100
char command_str[NUM] = { 0 };
char* myargv[OPTION] = { NULL };

int prev_sig = 0;
int prev_code = 0;


#define NO_REDIR 0
#define STDIN_REDIR 1
#define STDOUT_REDIR 2
#define APPEND_REDIR 3
int redir_type = NO_REDIR;
char* redir_file = NULL;
#define TrimSpace(start) do{\
                             while(*start == ' ') start++;\
                                }while(0)

void CommandCheck(char* commandline)
{
    char* start = commandline;
    char* end = commandline + strlen(commandline);

    while (start < end)
    {
        if (*start == '>')
        {
            *start = '\0';//字符串分割
            start++;
            if (*start != '>')
            {
                //输出重定向
                redir_type = STDOUT_REDIR;
            }
            else if (*start == '>')
            {
                //追加输出重定向
                redir_type = APPEND_REDIR;
                start++;
            }

            //去空格符
            TrimSpace(start);
            redir_file = start;
            break;
        }
        else if (*start == '<')
        {
            //输入重定向
            *start = '\0';
            start++;
            redir_type = STDIN_REDIR;

            //去空格符
            TrimSpace(start);
            redir_file = start;
            break;
        }
        else
        {
            start++;
        }

    }
}

int main()
{
    while (1)
    {
        errno = 0;
        memset(command_str, sizeof(command_str), '\0');
        //打印命令提示符
        printf("[用户名@操作系统版本 当前目录]#");
        fflush(stdout);//将缓冲区数据刷新到屏幕上

        //用户输入，捕获到CommandStr[]中。注意：输入的时候有\n到内存的数组内
        char* pchar = fgets(command_str, sizeof(command_str) - 1, stdin);//fgets函数会自动添加'\0'
        assert(pchar != NULL);

        command_str[strlen(command_str) - 1] = 0;//将command_str[]内的\n覆盖掉
        //printf("%s\n", command_str);//command_str[]内自带一个\n



        //先进行字符串检查，需不需要重定向，检查之前重置重定向类型
        redir_type = NO_REDIR;
        redir_file = NULL;
        CommandCheck(command_str);




        //字符数组切割转换成 char* argv[] 便于 execve() 传参
        myargv[0] = strtok(command_str, " ");

        int i = 1;
        //如果没有子串了，strtok -> NULL, myargv[end] = NULL
        while (myargv[i++] = strtok(NULL, " "));



        //命令是"ls"时，自动添加"--color=auto"
        if (myargv[0] != NULL && strcmp(myargv[0], "ls") == 0) {
            myargv[i - 1] = (char*)"--color=auto";
            myargv[i] = NULL;
        }

        if (myargv[0] != NULL && strcmp(myargv[0], "ll") == 0) {
            myargv[0] = (char*)"ls";
            int i = 0;
            while (myargv[i++] != NULL) { ; }
            myargv[i - 1] = (char*)"-l";
            myargv[i] = NULL;
        }

        //cd命令，这种不需要创建子进程的命令，让shell自己执行对应的命令，本质上就是执行系统接口
        //这种不需要创建子进程的命令，就叫做 内建/内置 命令。  echo 能打印本地变量就是因为是内置命令，shell里的变量也能找到
        if (myargv[0] != NULL && strcmp(myargv[0], "cd") == 0) {
            if (myargv[1] != NULL) {
                chdir(myargv[1]);
                continue;
            }
            else {
                printf("命令格式错误\n");
                continue;
            }
        }

        //内置命令 echo
        if (myargv[0] != NULL && strcmp(myargv[0], "echo") == 0 && myargv[1] != NULL) {
            if (strcmp(myargv[1], "$?") == 0) {
                printf("%d %d\n", prev_code, prev_sig);
                continue;
            }
            else {
                int count2 = 1;
                while (myargv[count2] != NULL)
                    printf("%s", myargv[count2++]);
                printf("\n");
                continue;
            }
        }


        //创建子进程
        pid_t id = fork();
        if (id < 0) {
            //创建子进程失败
            printf("子进程创建失败\n");
        }
        else if (id > 0) {
            //父进程获取子进程退出码

            //重置退出码
            prev_code = 0;
            prev_sig = 0;
            int status = 0;
            while (1) {
                pid_t id2 = waitpid(id, &status, WNOHANG);
                if (id2 == id)
                    break;
                else if (id2 == -1) {
                    strerror(errno);
                    break;
                }
            }
            prev_code = ((status >> 8) & 0xFF);
            prev_sig = (status & 0x7F);
        }
        else {
            //子进程替换之前要进行重定向，不然进程替换之后就执行进程，执行结束强制刷新缓冲区，再替换就没有意义了
            switch (redir_type)
            {
                case NO_REDIR:
                    //不需要重定向
                    break;
                case STDIN_REDIR:
                {
                    //输入重定向
                    int fd1 = open(redir_file, O_RDONLY);
                    if (fd1 < 0)
                    {
                        perror("open");
                        exit(errno);
                    }
                    dup2(fd1, 0);
                    break;
                }
                case STDOUT_REDIR:
                    //输出重定向
                case APPEND_REDIR:
                    //追加重定向
                {
                    int flags = O_WRONLY | O_CREAT;
                    if (redir_type == APPEND_REDIR)
                        flags |= O_APPEND;
                    else
                        flags |= O_TRUNC;
                    int fd2 = open(redir_file, flags, 0666);
                    if (fd2 < 0)
                    {
                        perror("open");
                        exit(errno);
                    }
                    dup2(fd2, 1);
                    break;
                }
                default:
                    printf("程序有问题\n");
                    break;
            }

            //子进程替换，只替换用户空间的代码和数据
            i = execvp(myargv[0], myargv);
            if (i == -1) {
                printf("子进程切换失败\n");
            }
            exit(1);
        }
    }
    return 0;
}



